import 'reflect-metadata'
import express,{Express, Request as ExpressRequest, Response as ExpressResponse, NextFunction } from 'express'
import {Logger} from './logger'
import path from 'path'
import {INJECT_TOKENS, DESIGN_PARAMTYPES, defineModule} from '@nestjs/common'
import { RequestMethod } from '@nestjs/common'
import { ArgumentsHost } from '@nestjs/common/arguments-host.interface'
import { GlobalHttpExceptionFilter } from '@nestjs/common/http-exception.filter'
import { APP_FILTER } from './constants'

export class NestApplication {
    private readonly app: Express = express()


    private readonly providerInstances = new Map()
    private readonly globalProviders = new Set()
    private readonly moduleProviers = new Map()
    private readonly middlewares = []
    private readonly excludeRoutes = []
    private readonly defaultGlobalHttpExceptionFilter = new GlobalHttpExceptionFilter()
    private readonly globalHttpExceptionFilters = []

    constructor(protected readonly module){
        this.app.use(express.json())
        this.app.use(express.urlencoded({extended: true}))
    }
    useGlobalFilters(...filters) {
        defineModule(this.module, filters)
        this.globalHttpExceptionFilters.push(...filters)
    }
    exclude(...routeInfos) {
        this.excludeRoutes.push(...routeInfos.map(this.normalizeRouteInfo))
        return this
    }
    apply(...middleware) {
        defineModule(this.module, middleware)
        this.middlewares.push(...middleware)
        return this
    }
    
    getMiddlewareInstance(middleware) {

        if (middleware instanceof Function) {
            const dependencies = this.resolveDependencies(middleware)
            return new middleware(...dependencies)
        }
        return middleware
    }
    isExcluded(reqPath, method) {
        return this.excludeRoutes.some(routeInfo => {
            const {routePath, routeMethod} =routeInfo
            return routePath === reqPath && (routeMethod === RequestMethod.ALL || routeMethod === method)
        })
    }
    forRoutes(...routes){
        for(const route of routes) {
            for(const middleware of this.middlewares) {
                const {routePath,routeMethod } = this.normalizeRouteInfo(route)
                this.app.use(routePath,(req,res,next) => {
                    if (this.isExcluded(req.originalUrl,req.method)) {
                        return next()
                    }
                    if (routeMethod === RequestMethod.ALL || routeMethod == req.method) {
                        if ('use' in middleware.prototype || 'use' in middleware) {
                            const middlewareInstance = this.getMiddlewareInstance(middleware)
                            middlewareInstance.use(req,res,next)
                        } else if (middleware instanceof Function) {
                            middleware(req,res,next)
                        } else {
                            next()
                        }   
                        
                    } else {
                        next()
                    }
                })
            }
        }
        this.middlewares.length = 0
        return this
    }
    normalizeRouteInfo(route) {
        let routePath = ''
        let routeMethod = RequestMethod.ALL
        if (typeof route === 'string') {
            routePath = route
        } else if ('path' in route) {
            routePath = route.path
            routeMethod = route.method ?? RequestMethod.ALL
        } else if (route instanceof Function) {
            routePath = Reflect.getMetadata('prefix', route)
        }
        routePath = path.posix.join('/', routePath)
        return {
            routePath,
            routeMethod
        }
    }
   async initMiddlewares() {
        await this.module.prototype.configure?.(this)
    }
    async initProviders(){
        const imports = Reflect.getMetadata('imports', this.module)??[]
 
        for(let importModule of imports) {
            let importedModule = importModule
            if (importedModule instanceof Promise) {
                importedModule = await importedModule
            }
            if ('module' in importedModule) {
                const {module, providers, controllers, exports} = importedModule
                const oldControllers = Reflect.getMetadata('controllers', module)
                const newControllers = [...(oldControllers??[]),...(controllers??[])]
                defineModule(module, newControllers)
                const oldProviders = Reflect.getMetadata('providers', module)
                const newProviders = [...(oldProviders??[]),...(providers??[])]
                defineModule(module, newProviders)
                const oldExports = Reflect.getMetadata('exports', module)
                const newExports = [...(oldExports??[]),...(exports??[])]
                Reflect.defineMetadata('controllers', newControllers, module)
                Reflect.defineMetadata('providers', newProviders, module)
                Reflect.defineMetadata('exports', newExports, module)
                this.registerProvidersFromModule(module, this.module)
            } else {
                this.registerProvidersFromModule(importedModule, this.module)
            }
            
        }
        const providers = Reflect.getMetadata('providers', this.module) ?? []
        for (const provider of providers) {
            this.addProvider(provider, this.module)
        }
    }
    private registerProvidersFromModule(module, ...parentModules){
        const global = Reflect.getMetadata('global', module)
        const importedProviders = Reflect.getMetadata('providers', module) ?? []

        const exports = Reflect.getMetadata('exports', module) ?? []
        for(const exportToken of exports) {
            if (this.isModule(exportToken)){
                this.registerProvidersFromModule(exportToken, module, ...parentModules)
            } else {
                const provider = importedProviders.find(provider=>provider===exportToken|| provider.provide===exportToken)
                if (provider) {
                    [module, ...parentModules].forEach((module) => {
                        this.addProvider(provider, module, global)
                    })
                    
                }
            }   
        }
        this.initController(module)
    }
    private isModule(exportToken){
        return exportToken && exportToken instanceof Function && Reflect.getMetadata('isModule', exportToken)
    }
    addProvider(provider, module, global = false){
       
        // if (this.providers.has(injectToken)) return
        const providers = global ? this.globalProviders : (this.moduleProviers.get(module)|| new Set())
        if (!global) {
            this.moduleProviers.set(module, providers)
        }
        this.moduleProviers.set(module, providers)
        const injectToken = provider.provide??provider
        if (this.providerInstances.has(injectToken)) {
            providers.add(injectToken)
            return
        }
        if (provider.provide && provider.useClass) {
            
            const Clazz = provider.useClass
            const dependencies = this.resolveDependencies(Clazz)
            const value = new Clazz(...dependencies)
            this.providerInstances.set(provider.provide, value)
            providers.add(provider.provide)
        } else if (provider.provide && provider.useValue) {
            this.providerInstances.set(provider.provide, provider.useValue)
            providers.add(provider.provide)
        } else if (provider.provide && provider.useFactory) {
            const inject = provider.inject??[]
            const injectedValues = inject.map(injectToken => this.getProviderByToken(injectToken, module))
            const value = provider.useFactory(...injectedValues)
            this.providerInstances.set(provider.provide,value)
            providers.add(provider.provide)
        } else {
            const dependencies = this.resolveDependencies(provider)
            const value = new provider(...dependencies)
            this.providerInstances.set(provider, value)
            providers.add(provider)
        }
    }
    use(middleware){
        this.app.use(middleware)
    }
    private getProviderByToken(injectedToken, module){
        if (this.moduleProviers.get(module)?.has(injectedToken) || this.globalProviders.has(injectedToken)) {
            return this.providerInstances.get(injectedToken)
        } else {
            return null
        }
    }
    // 获取类的依赖也就是参数
    private resolveDependencies(clazz){
        // 获取inject.decorator 注入
        const injectTokens = Reflect.getMetadata(INJECT_TOKENS, clazz) ?? []
        // 获取injectable.decorator注入
        const constructorParams = Reflect.getMetadata(DESIGN_PARAMTYPES, clazz) ?? []
        // console.log('injectTokens', injectTokens, constructorParams)

        return constructorParams.map((param,index) => {
            const module = Reflect.getMetadata('module', clazz)
            return this.getProviderByToken(injectTokens[index] ?? param, module)
        })
    }
    async initController (module){
        const controllers = Reflect.getMetadata('controllers', module) || []
        Logger.log(`AppModule dependencies initialized`, 'InstanceLoader')
        for (let Controller of controllers) {
            const dependencies = this.resolveDependencies(Controller)

            const controller = new Controller(...dependencies)
            const prefix = Reflect.getMetadata('prefix', Controller) || '/'
            Logger.log(`${Controller.name} {${prefix}}`, 'RoutesResolver')

            const constrollerPrototype = Controller.prototype
            const controllerFilters = Reflect.getMetadata('filters',Controller) || []
            defineModule(this.module,controllerFilters)
            for (let methodName of Object.getOwnPropertyNames(constrollerPrototype)) {
                const method = constrollerPrototype[methodName]

                const httpMethod = Reflect.getMetadata('method', method)
                const pathMetadata = Reflect.getMetadata('path', method)
                const redirectUrl = Reflect.getMetadata('redirectUrl', method)
                const redirectStatusCode = Reflect.getMetadata('redirectStatusCode', method)
                const statusCode = Reflect.getMetadata('statusCode', method)
                const headers = Reflect.getMetadata('headers', method) || []
                const methodFilters = Reflect.getMetadata('filters',method) || []
                defineModule(this.module,methodFilters)
                if (!httpMethod) {
                    continue
                }
                const routePath = path.posix.join('/', prefix, pathMetadata)
                this.app[httpMethod.toLowerCase()](routePath, async (req: ExpressRequest,res: ExpressResponse,next: NextFunction) => {
                    const host:ArgumentsHost = {
                        switchToHttp: ()=>({
                            getRequest:()=>req,
                            getResponse: () => res,
                            getNext: () => next
                        })
                    }
                    try {
                        const args = this.resolveParams(controller, methodName, req, res, next, host)
                        const result = method.call(controller, ...args)
                        if (result?.url) {
                            return res.redirect(result.statusCode||302, result.url)
                        }
                        if (redirectUrl) {
                            return res.redirect(redirectStatusCode, redirectUrl)
                        }
                        if (statusCode) {
                            res.statusCode = statusCode
                        } else if (httpMethod === 'POST'){
                            res.statusCode = 201
                        }

                        const responseMetadata = this.getResponseMetadata(controller, methodName)
                        if (!responseMetadata || (responseMetadata?.data?.passthrough)) {
                            headers.forEach(header => {
                                res.setHeader(header.name, header.value)
                            });
                            res.send(result)
                        }
                    } catch (error) {
                        await this.callExceptionFilters(error, host, methodFilters, controllerFilters)
                    }
                    
                    
                })
                // [RouterExplorer] Mapped {/, GET} 
                Logger.log(`Mapped {${routePath}, ${httpMethod}} route`, 'RouterExplorer')
            }
            Logger.log(`Nest application successfully started`, 'NestApplication')
        }
    }
    getFilterInstance(filter) {

        if (filter instanceof Function) {
            const dependencies = this.resolveDependencies(filter)
            return new filter(...dependencies)
        }
        return filter
    }
    private callExceptionFilters(error, host, methodFilters, controllerFilters) {
        const allFilters = [...methodFilters,...controllerFilters,...this.globalHttpExceptionFilters,this.defaultGlobalHttpExceptionFilter]
        for(const filter of allFilters) {
            let filterInstance = this.getFilterInstance(filter)
            const exceptions = Reflect.getMetadata('catch',filterInstance.constructor)??[]
            if (exceptions.length === 0 || exceptions.some(exception=>error instanceof exception)) {
                filterInstance.catch(error, host)
                break
            }
            
        }
    }
    private getResponseMetadata(controller, methodName) {
        const paramsMetadata = Reflect.getMetadata(`params`, controller,methodName) ?? []
        return paramsMetadata.filter(Boolean).find((param) =>param.key ==='Response' || param.key ==='Res' || param.key ==='Next')
    }
    private resolveParams(instance, methodName, req, res, next, host){
       const paramsMetadata = Reflect.getMetadata(`params`, instance,methodName) ?? []

       return paramsMetadata.map((paramMetadata) => {
            const {key, data, factory, pipes} = paramMetadata
            let value;
            switch(key){
                case 'Request':
                case 'Req':
                    value = req
                    break
                case 'Query':
                    value = data ? req.query[data] : req.query
                    break
                case 'Headers':
                    value = data ? req.headers[data] : req.headers
                    break
                case 'Session':
                    value = data ? req.session[data] : req.session
                    break
                case 'Ip':
                    value = req.ip
                    break
                case 'Param':
                    value = data ? req.params[data] : req.params
                    break
                case 'Body':
                    value = data ? req.body[data] : req.body
                    break
                case 'Response':
                case 'Res':
                    value = res
                    break
                case 'Next':
                    value = next
                    break
                case 'DecoratorFactory':
                    value = factory(data, host)
                    break
                default:
                    value = null
            }
            for (const pipe of [...pipes]) {
                const pipeInstance = this.getPipeInstance(pipe)
                value = pipeInstance.transform(value)
            }
            return value
       })
    }
     getPipeInstance(pipe) {
        if (pipe instanceof Function) {
            const dependencies = this.resolveDependencies(pipe)
            return new pipe(...dependencies)
        }
        return pipe
    }
    async initGlobalFilters() {
        const providers = Reflect.getMetadata('providers', this.module) ?? []
        for (const provider of providers) {
            if (provider.provide ===APP_FILTER) {
                const proInstance = this.getProviderByToken(APP_FILTER, this.module)
                this.useGlobalFilters(proInstance)
            }
        }
    }
    async listen(port){
        await this.initProviders() // 注入providers
        await this.initMiddlewares()
        await this.initGlobalFilters()
        await this.initController(this.module)
        this.app.listen(port, () => {
            Logger.log(`Application is running on http://localhost:${port}`,this.constructor.name)
        })
    }

}